/* Copyright (c) 2012 William Steven Knauer
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software
 * and associated documentation files (the "Software"), to deal in the Software without restriction,
 * including without limitation the rights to use, copy, modify, merge, publish, distribute,
 * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all copies or
 * substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
 * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
 * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

package com.williamknauer.android.yagol;

import java.util.Stack;

import android.os.Parcel;
import android.os.Parcelable;
import android.os.SystemClock;

/**
 * Represents the state of the model used to render the game.
 * 
 * @author William S Knauer <knauerw1@gmail.com>
 * @version 2012.0111
 */
public class GameModel implements Parcelable {
	private final int mRows;
	private final int mCols;
	private final Stack<int[]> mUndoStack;
	private final int mUndoCapacity;

	public static final long EPOCH = SystemClock.uptimeMillis();
	public static final int PREF_ON = 1;
	public static final int PREF_OFF = 0;
	public static final boolean CELL_ALIVE = true;
	public static final boolean CELL_DEAD = false;

	private volatile boolean[] mCells;
	private volatile boolean[] mWorkspace;
	private volatile int[] mCellTimes;
	private volatile int mPopSize;

	private volatile int mPrefSimulationSpeed;
	private volatile int mPrefFadeinTimeout;
	private volatile int mPrefFadeinDuration;
	private volatile int mPrefFadeoutDuration;
	private volatile int mPrefColorCell;
	private volatile int mPrefColorFadein;
	private volatile int mPrefColorBackground;

	public final int getPrefSimulationSpeed() {
		return mPrefSimulationSpeed;
	}

	public final int getPrefFadeinTimeout() {
		return mPrefFadeinTimeout;
	}

	public final int getPrefFadeinDuration() {
		return mPrefFadeinDuration;
	}

	public final int getPrefFadeoutDuration() {
		return mPrefFadeoutDuration;
	}

	public final int getPrefColorCell() {
		return mPrefColorCell;
	}

	public final int getPrefColorFadein() {
		return mPrefColorFadein;
	}

	public final int getPrefColorBackground() {
		return mPrefColorBackground;
	}

	public final synchronized void setPrefs(int simulationSpeed, int fadeinTimeout,
			int fadeinDuration, int fadeoutDuration, int colorCell, int colorFadein,
			int colorBackground) {
		mPrefSimulationSpeed = simulationSpeed;
		mPrefFadeinTimeout = fadeinTimeout;
		mPrefFadeinDuration = fadeinDuration;
		mPrefFadeoutDuration = fadeoutDuration;
		mPrefColorCell = colorCell;
		mPrefColorFadein = colorFadein;
		mPrefColorBackground = colorBackground;
	}

	public GameModel(int rows, int cols) {
		setPrefs(0, 0, 0, 0, 0, 0, 0);
		mRows = rows;
		mCols = cols;
		mUndoCapacity = 100;
		mUndoStack = new Stack<int[]>();
		mPopSize = 0;

		mCells = new boolean[rows * cols];
		mWorkspace = new boolean[rows * cols];
		mCellTimes = new int[rows * cols];
		clear();
	}

	public synchronized void clear() {
		/*
		 * Set all cells to dead.
		 */
		for (int i = 0; i < mRows * mCols; i++) {
			mCells[i] = CELL_DEAD;
			mCellTimes[i] = 0;
			mPopSize = 0;
		}
	}

	private synchronized void clearWorkspace() {
		for (int i = 0; i < mRows * mCols; i++) {
			mWorkspace[i] = CELL_DEAD;
		}
	}

	public synchronized void flip(int index) {
		/*
		 * Flip the cell's life/death status.
		 */
		if (mCells[index] == CELL_ALIVE) {
			mCells[index] = CELL_DEAD;
			mCellTimes[index] = 0;
			mPopSize--;
		} else {
			mCells[index] = CELL_ALIVE;
			mCellTimes[index] = 0;
			mPopSize++;
		}
	}

	public final int indexOf(int row, int col) {
		return row * mCols + col;
	}

	public final int getCols() {
		return mCols;
	}

	private final int getNeighborCount(int index) {
		int row = index / mCols;
		int col = index % mCols;
		int neighbors = 0;

		/*
		 * Check all neighbors starting at north going clockwise.
		 */
		if (row - 1 >= 0 && mCells[(row - 1) * mCols + col] == CELL_ALIVE)
			neighbors++;
		if (row - 1 >= 0 && col + 1 < mCols && mCells[(row - 1) * mCols + col + 1] == CELL_ALIVE)
			neighbors++;
		if (col + 1 < mCols && mCells[(row) * mCols + col + 1] == CELL_ALIVE)
			neighbors++;
		if (row + 1 < mRows && col + 1 < mCols && mCells[(row + 1) * mCols + col + 1] == CELL_ALIVE)
			neighbors++;
		if (row + 1 < mRows && mCells[(row + 1) * mCols + col] == CELL_ALIVE)
			neighbors++;
		if (row + 1 < mRows && col - 1 >= 0 && mCells[(row + 1) * mCols + col - 1] == CELL_ALIVE)
			neighbors++;
		if (col - 1 >= 0 && mCells[(row) * mCols + col - 1] == CELL_ALIVE)
			neighbors++;
		if (row - 1 >= 0 && col - 1 >= 0 && mCells[(row - 1) * mCols + col - 1] == CELL_ALIVE)
			neighbors++;
		return neighbors;
	}

	public final int getRows() {
		return mRows;
	}

	public synchronized void pushUndo() {
		if (mUndoStack.size() == mUndoCapacity) {
			/*
			 * Remove the oldest undo state from the stack.
			 */
			mUndoStack.remove(0);
		}

		/*
		 * Add the current state to the stack.
		 */
		int[] state = new int[mPopSize];
		int sIndex = 0;
		for (int i = 0; i < mRows * mCols; i++)
			if (mCells[i] == CELL_ALIVE)
				state[sIndex++] = i;
		mUndoStack.push(state);
	}

	public synchronized void popUndo() {
		if (mUndoStack.isEmpty())
			return;

		/*
		 * Load the most recent saved state from the stack.
		 */
		clear();
		int[] state = mUndoStack.pop();
		for (int i : state)
			mCells[i] = CELL_ALIVE;
		mPopSize = state.length;
	}

	public synchronized void update() {
		/*
		 * Get the current time from epoch.
		 */
		int time = (int) (SystemClock.uptimeMillis() - EPOCH);

		/*
		 * For each cell...
		 */
		for (int i = 0; i < mRows * mCols; i++) {
			boolean cell = mCells[i];

			/*
			 * Count the cell's neighbors.
			 */
			int neighbors = getNeighborCount(i);

			/*
			 * Determine whether the cell lives or dies.
			 */
			if (cell == CELL_ALIVE) {
				mWorkspace[i] = (neighbors == 2 || neighbors == 3) ? CELL_ALIVE : CELL_DEAD;
			} else if (neighbors == 3) {
				mWorkspace[i] = CELL_ALIVE;
			}

			if (cell == CELL_ALIVE && mWorkspace[i] == CELL_DEAD) {
				/*
				 * Cell has just died. Log the time of death and update the
				 * census.
				 */
				cell = CELL_DEAD;
				mCellTimes[i] = time;
				mPopSize--;
			} else if (cell == CELL_DEAD && mWorkspace[i] == CELL_ALIVE) {
				/*
				 * Cell was just born. Log the time of birth and update the
				 * census.
				 */
				cell = CELL_ALIVE;
				mCellTimes[i] = time;
				mPopSize++;
			} else {
				/*
				 * Cell state has not changed. Just copy the state to the
				 * workspace.
				 */
				mWorkspace[i] = cell;
			}
		}

		/*
		 * Swap the workspace with the model.
		 */
		boolean[] temp = mCells;
		mCells = mWorkspace;
		mWorkspace = temp;

		/*
		 * Clear the workspace.
		 */
		clearWorkspace();
	}

	public final boolean getCell(int i) {
		return mCells[i];
	}

	public final int getCellTime(int i) {
		return mCellTimes[i];
	}

	@Override
	public int describeContents() {
		return 0;
	}

	@Override
	public void writeToParcel(Parcel dest, int flags) {
		/*
		 * Dump the GameModel state to a parcel.
		 */
		dest.writeInt(mRows);
		dest.writeInt(mCols);
		dest.writeInt(mUndoCapacity);
		dest.writeInt(mUndoStack.size());
		while (!mUndoStack.isEmpty()) {
			int[] undo = mUndoStack.pop();
			dest.writeInt(undo.length);
			dest.writeIntArray(undo);
		}
		dest.writeInt(mCells.length);
		dest.writeBooleanArray(mCells);
		dest.writeInt(mPopSize);
		dest.writeInt(mPrefSimulationSpeed);
		dest.writeInt(mPrefFadeinTimeout);
		dest.writeInt(mPrefFadeinDuration);
		dest.writeInt(mPrefFadeoutDuration);
		dest.writeInt(mPrefColorCell);
		dest.writeInt(mPrefColorFadein);
		dest.writeInt(mPrefColorBackground);
	}

	private GameModel(Parcel source) {
		/*
		 * Read back state data in the order it was dumped to the parcel.
		 */
		mRows = source.readInt();
		mCols = source.readInt();
		mUndoCapacity = source.readInt();
		mUndoStack = new Stack<int[]>();
		int size = source.readInt();
		for (int i = 0; i < size; i++) {
			int[] undo = new int[source.readInt()];
			source.readIntArray(undo);
			mUndoStack.add(0, undo);
		}
		mCells = new boolean[source.readInt()];
		source.readBooleanArray(mCells);
		mPopSize = source.readInt();
		mPrefSimulationSpeed = source.readInt();
		mPrefFadeinTimeout = source.readInt();
		mPrefFadeinDuration = source.readInt();
		mPrefFadeoutDuration = source.readInt();
		mPrefColorCell = source.readInt();
		mPrefColorFadein = source.readInt();
		mPrefColorBackground = source.readInt();
		mWorkspace = new boolean[mCells.length];
		mCellTimes = new int[mCells.length];
	}

	public static final Parcelable.Creator<GameModel> CREATOR = new Parcelable.Creator<GameModel>() {

		@Override
		public GameModel createFromParcel(Parcel source) {
			return new GameModel(source);
		}

		@Override
		public GameModel[] newArray(int size) {
			return new GameModel[size];
		}

	};
}